﻿using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore;
using System.Data;
using System.Linq.Expressions;
using System.Text;
using Zack.EFCore.Batch.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace AiQiuQuan.Sport.Core
{
    /// <summary>
    /// efcore delete extensions
    /// </summary>
    public static class EntityFrameworkCoreDeleteExtensions
    {
        /// <summary>
        /// efcore lamada 简单语句删除
        /// </summary>
        public static async Task<int> DeleteAsync<TEntity>(this DbContext ctx,
            Expression<Func<TEntity, bool>> predicate = null, bool ignoreQueryFilters = false, CancellationToken cancellationToken = default)
            where TEntity : class
        {
            var dbSet = ctx.Set<TEntity>();
            var sql = GenerateDeleteSQL(ctx, dbSet, predicate, ignoreQueryFilters, out var parameters);
            return await ExecuteSQLAsync(ctx, sql, parameters, cancellationToken);
        }

        #region private methods
        /// <summary>
        /// GenerateDeleteSQL
        /// </summary>
        private static string GenerateDeleteSQL<TEntity>(DbContext ctx, IQueryable<TEntity> queryable, Expression<Func<TEntity, bool>> predicate, bool ignoreQueryFilters,
            out IDictionary<string, object> parameters) where TEntity : class
        {
            queryable = predicate != null ? queryable.Where(predicate) : queryable.Where(e => 1 == 1);
            if (ignoreQueryFilters)
            {
                queryable = queryable.IgnoreQueryFilters();
            }

            var parsingResult = queryable.Parse(ctx, ignoreQueryFilters);
            var sqlGenHelpr = ctx.GetService<ISqlGenerationHelper>();
            var tableName = sqlGenHelpr.DelimitIdentifier(parsingResult.TableName, parsingResult.Schema);
            var sbSQL = new StringBuilder();
            sbSQL.Append("DELETE FROM ").Append(tableName);
            if (!string.IsNullOrWhiteSpace(parsingResult.PredicateSQL))
            {
                //like DeleteRangeAsync<Comment>(c => c.Article.Id == id);
                if (!parsingResult.FullSQL.Contains("join", StringComparison.OrdinalIgnoreCase))
                {
                    sbSQL.Append(" WHERE ").Append(parsingResult.PredicateSQL);
                }
                else
                {
                    var aliasSeparator = parsingResult.QuerySqlGenerator.P_AliasSeparator;
                    sbSQL.Append(" WHERE ").Append(BatchUtils.BuildWhereSubQuery(queryable, ctx, aliasSeparator));
                }
            }

            parameters = parsingResult.Parameters;
            foreach (var item in parameters)
            {
                if (item.Value != null)
                {
                    var valueType = item.Value.GetType();
                    if (valueType == typeof(DateTimeOffset) || valueType == typeof(DateTimeOffset?))
                    {
                        parameters[item.Key] = ((DateTimeOffset)item.Value).DateTime;
                    }
                }
            }

            return sbSQL.ToString();
        }

        private static async Task<int> ExecuteSQLAsync(DbContext ctx, string sql, IDictionary<string, object> parameters, CancellationToken cancellationToken)
        {
            var conn = ctx.Database.GetDbConnection();
            await conn.OpenIfNeededAsync(cancellationToken);
            using (var cmd = conn.CreateCommand())
            {
                cmd.ApplyCurrentTransaction(ctx);
                cmd.CommandText = sql;
                cmd.AddParameters(ctx, parameters);
                return await cmd.ExecuteNonQueryAsync(cancellationToken);
            }
        }

        public static void ApplyCurrentTransaction(this IDbCommand cmd, DbContext dbContext)
        {
            var tx = dbContext.Database.CurrentTransaction;
            if (tx != null)
            {
                cmd.Transaction = tx.GetDbTransaction();
            }
        }

        public static void AddParameters(this IDbCommand cmd, DbContext ctx, IDictionary<string, object> parameters)
        {
            var typeMapping = ctx.GetService<IRelationalTypeMappingSource>();
            foreach (var p in parameters)
            {
                if (p.Value != null)
                {
                    var mappedType = typeMapping.FindMapping(p.Value.GetType());
                    //the parameter type is not supported by underlying database.
                    //the value may be EF.Functions.ContainsOrEqual, int[] that have been translated into SQL clause.
                    //like Where(m => EF.Functions.ContainsOrEqual(m.IPv4.Value, ip)), and Where(p=>ids.Contains(p.Id)),
                    //so it's ignored.
                    if (mappedType == null)
                    {
                        continue;
                    }
                }

                var dbParam = cmd.CreateParameter();
                dbParam.ParameterName = p.Key;
                dbParam.Value = p.Value ?? DBNull.Value;
                cmd.Parameters.Add(dbParam);
            }
        }
        #endregion
    }
}
